Imports System
Imports System.IO
Imports System.Net.Http
Imports System.Text
Imports System.Threading.Tasks
Imports System.Configuration
Imports System.Web.Script.Serialization
Imports System.Collections.Generic
Imports System.Windows.Forms
Imports System.Net
Imports System.Net.Http.Headers
Imports System.Net.Sockets

Public Class Form1
    Inherits Form

    Private WithEvents web As Microsoft.Web.WebView2.WinForms.WebView2
    Private http As HttpClient
    Private baseUrl As String
    Private model As String
    Private apiKey As String
    Private streamEnabled As Boolean
    Private sessionId As String = Guid.NewGuid().ToString("N")

    Public Sub New()
        Me.Text = "QwenWin · VB.NET + WebView2 (.NET 4.7.2)"
        Me.Width = 1200
        Me.Height = 800
        Me.StartPosition = FormStartPosition.CenterScreen

        web = New Microsoft.Web.WebView2.WinForms.WebView2() With {
            .Dock = DockStyle.Fill
        }
        Me.Controls.Add(web)

        http = New HttpClient()

        Dim cfgApi = ConfigurationManager.AppSettings("DASHSCOPE_API_KEY")
        Dim envApi = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY")
        apiKey = If(String.IsNullOrWhiteSpace(envApi), cfgApi, envApi)
        If String.IsNullOrWhiteSpace(apiKey) Then
            MessageBox.Show("请在 App.config 设置 DASHSCOPE_API_KEY，或通过环境变量 DASHSCOPE_API_KEY 提供。", "缺少 API Key", MessageBoxButtons.OK, MessageBoxIcon.Warning)
        End If

        baseUrl = ConfigurationManager.AppSettings("QWEN_BASE_URL")
        If String.IsNullOrWhiteSpace(baseUrl) Then baseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1"

        model = ConfigurationManager.AppSettings("QWEN_MODEL")
        If String.IsNullOrWhiteSpace(model) Then model = "qwen-plus"

        Dim s = ConfigurationManager.AppSettings("STREAM")
        streamEnabled = String.Equals(s, "true", StringComparison.OrdinalIgnoreCase)

        InitializeAsync()
    End Sub

    Private Async Sub InitializeAsync()
        Await web.EnsureCoreWebView2Async(Nothing)

        ' map /appassets/ to local www folder
        Dim wwwPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "www")
        web.CoreWebView2.SetVirtualHostNameToFolderMapping("appassets", wwwPath, Microsoft.Web.WebView2.Core.CoreWebView2HostResourceAccessKind.Allow)
        web.Source = New Uri("https://appassets/index.html")

        AddHandler web.CoreWebView2.WebMessageReceived, AddressOf OnWebMessageReceived
    End Sub

    Private Sub OnWebMessageReceived(
                                     ByVal sender As Object,
                                     ByVal e As Microsoft.Web.WebView2.Core.CoreWebView2WebMessageReceivedEventArgs)

        ' 1) 取文本
        Dim jsonText As String = e.TryGetWebMessageAsString()
        If String.IsNullOrEmpty(jsonText) Then
            Exit Sub
        End If

        ' 2) 反序列化为 Dictionary(Of String, Object)
        Dim ser As New System.Web.Script.Serialization.JavaScriptSerializer()
        Dim msg As Dictionary(Of String, Object) =
                TryCast(ser.DeserializeObject(jsonText), Dictionary(Of String, Object))
        If msg Is Nothing Then
            Exit Sub
        End If

        ' 3) 安全读取 "type"
        Dim t As String = Nothing
        If msg.ContainsKey("type") AndAlso msg("type") IsNot Nothing Then
            t = Convert.ToString(msg("type"))
        Else
            Exit Sub
        End If

        ' 4) 分支处理 —— 用 Select Case 更清晰
        Select Case t
            Case "ask"
                Dim userText As String = ""
                If msg.ContainsKey("text") AndAlso msg("text") IsNot Nothing Then
                    userText = Convert.ToString(msg("text"))
                End If

                Dim sid As String = sessionId
                If msg.ContainsKey("sessionId") AndAlso msg("sessionId") IsNot Nothing Then
                    sid = Convert.ToString(msg("sessionId"))
                End If

                Dim _ignore1 = HandleUserAskAsync(sid, userText) ' fire & forget

            Case "loadSessions"
                Dim _ignore2 = SendSessionsAsync() ' fire & forget

            Case "openSession"
                Dim sid As String = ""
                If msg.ContainsKey("sessionId") AndAlso msg("sessionId") IsNot Nothing Then
                    sid = Convert.ToString(msg("sessionId"))
                End If
                Dim _ignore3 = LoadSessionAsync(sid) ' fire & forget

            Case Else
                ' 未知消息类型，忽略
        End Select
    End Sub


    Private Async Function HandleUserAskAsync(sid As String, userText As String) As Task
        sessionId = sid
        ' Echo user message to UI
        Await PostToWebAsync(New Dictionary(Of String, Object) From {
            {"type", "addUserMessage"},
            {"sessionId", sid},
            {"content", userText}
        })

        ' Compose messages from history
        Dim history = LoadSessionMessages(sid)
        history.Add(New Dictionary(Of String, Object) From {
            {"role", "user"},
            {"content", userText}
        })

        Dim err As String = Nothing
        Try
            If streamEnabled Then
                Await StreamChatAsync(history, sid)
            Else
                Await ChatOnceAsync(history, sid)
            End If
            SaveSessionMessages(sid, history)
        Catch ex As Exception
            err = "请求失败：" & ex.Message
        End Try

        If err IsNot Nothing Then
            Await PostToWebAsync(New Dictionary(Of String, Object) From {
                {"type", "addAssistantMessage"},
                {"sessionId", sid},
                {"content", err}
            })
        End If
    End Function

    Private Function MessagesFilePath(sid As String) As String
        Dim dirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sessions")
        If Not Directory.Exists(dirPath) Then Directory.CreateDirectory(dirPath)
        Return Path.Combine(dirPath, sid & ".json")
    End Function

    Private Function LoadSessionMessages(sid As String) As List(Of Dictionary(Of String, Object))
        Dim p = MessagesFilePath(sid)
        If System.IO.File.Exists(p) Then
            Dim txt = System.IO.File.ReadAllText(p, Encoding.UTF8)
            Dim ser As New JavaScriptSerializer()
            Dim arr = CType(ser.DeserializeObject(txt), Object())
            Dim list As New List(Of Dictionary(Of String, Object))()
            For Each o In arr
                list.Add(CType(o, Dictionary(Of String, Object)))
            Next
            Return list
        End If
        ' default system prompt
        Return New List(Of Dictionary(Of String, Object)) From {
            New Dictionary(Of String, Object) From {{"role", "system"}, {"content", "You are ChatGPT-style assistant (Qwen)."}}
        }
    End Function

    Private Sub SaveSessionMessages(sid As String, history As List(Of Dictionary(Of String, Object)))
        Dim p = MessagesFilePath(sid)
        Dim ser As New JavaScriptSerializer()
        Dim txt = ser.Serialize(history.ToArray())
        System.IO.File.WriteAllText(p, txt, Encoding.UTF8)
    End Sub

    Private Async Function SendSessionsAsync() As Task
        Dim dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sessions")
        If Not Directory.Exists(dir) Then Directory.CreateDirectory(dir)
        Dim items = Directory.GetFiles(dir, "*.json")
        Dim list As New List(Of Object)
        For Each f In items
            Dim sid = Path.GetFileNameWithoutExtension(f)
            list.Add(New Dictionary(Of String, Object) From {{"id", sid}, {"title", "会话 " & sid.Substring(0, 6)}})
        Next
        Await PostToWebAsync(New Dictionary(Of String, Object) From {{"type", "sessions"}, {"items", list}})
    End Function

    Private Async Function LoadSessionAsync(sid As String) As Task
        sessionId = sid
        Dim messages = LoadSessionMessages(sid)
        Await PostToWebAsync(New Dictionary(Of String, Object) From {
            {"type", "loadSession"},
            {"sessionId", sid},
            {"messages", messages}
        })
    End Function

    Private Async Function ChatOnceAsync(history As List(Of Dictionary(Of String, Object)), sid As String) As Task
        Dim url = baseUrl.TrimEnd("/"c) & "/chat/completions"
        Dim req As New HttpRequestMessage(HttpMethod.Post, url)
        req.Headers.Authorization = New Headers.AuthenticationHeaderValue("Bearer", apiKey)

        Dim body = New Dictionary(Of String, Object) From {
            {"model", model},
            {"messages", history.ToArray()},
            {"temperature", 0.7},
            {"stream", False}
        }
        Dim ser As New JavaScriptSerializer()
        req.Content = New StringContent(ser.Serialize(body), Encoding.UTF8, "application/json")

        Using resp = Await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead)
            Dim text = Await resp.Content.ReadAsStringAsync()
            If Not resp.IsSuccessStatusCode Then
                Throw New Exception("HTTP " & CInt(resp.StatusCode) & ": " & text)
            End If
            Dim json = CType(ser.DeserializeObject(text), Dictionary(Of String, Object))
            Dim choices = CType(json("choices"), Object())
            Dim content = CStr(CType(CType(choices(0), Dictionary(Of String, Object))("message"), Dictionary(Of String, Object))("content"))
            history.Add(New Dictionary(Of String, Object) From {{"role", "assistant"}, {"content", content}})
            Await PostToWebAsync(New Dictionary(Of String, Object) From {
                {"type", "addAssistantMessage"},
                {"sessionId", sid},
{"content", content}
})
        End Using
    End Function

Private Async Function StreamChatAsync(history As List(Of Dictionary(Of String, Object)), sid As String) As Task
    Dim url = baseUrl.TrimEnd("/"c) & "/chat/completions"

    Dim req As New HttpRequestMessage(HttpMethod.Post, url)
    req.Version = New Version(1, 1)
    req.Headers.Authorization = New AuthenticationHeaderValue("Bearer", apiKey)
    req.Headers.ExpectContinue = False
    req.Headers.ConnectionClose = False
    req.Headers.Accept.Clear()
    req.Headers.Accept.ParseAdd("text/event-stream")
    req.Headers.TryAddWithoutValidation("Cache-Control", "no-cache")

    Dim body = New Dictionary(Of String, Object) From {
        {"model", model},
        {"messages", history.ToArray()},
        {"temperature", 0.7},
        {"stream", True}
    }
    Dim ser As New JavaScriptSerializer()
    req.Content = New StringContent(ser.Serialize(body), Encoding.UTF8, "application/json")

    ' —— 关键：在外面准备一个错误消息容器，Catch 里只写入文本，不 Await ——
    Dim errorToPost As Dictionary(Of String, Object) = Nothing

    Try
        Using resp = Await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead)
            Dim stream = Await resp.Content.ReadAsStreamAsync()
            If Not resp.IsSuccessStatusCode Then
                Dim textErr As String = ""
                Using sr0 As New StreamReader(stream, Encoding.UTF8)
                    textErr = sr0.ReadToEnd()
                End Using
                Throw New Exception("HTTP " & CInt(resp.StatusCode) & ": " & textErr)
            End If

            Dim sb As New StringBuilder()
            Using sr As New StreamReader(stream, Encoding.UTF8)
                Dim line As String = Nothing
                Await PostToWebAsync(New Dictionary(Of String, Object) From {
                    {"type", "startAssistantMessage"}, {"sessionId", sid}
                })
                Do
                    line = Await sr.ReadLineAsync()
                    If line Is Nothing Then Exit Do
                    If line.StartsWith("data: ") Then
                        Dim payload = line.Substring(6).Trim()
                        If payload = "[DONE]" Then Exit Do
                        If payload.Length = 0 Then Continue Do
                        Try
                            Dim chunk = CType(ser.DeserializeObject(payload), Dictionary(Of String, Object))
                            If chunk.ContainsKey("choices") Then
                                Dim choices = CType(chunk("choices"), Object())
                                If choices.Length > 0 Then
                                    Dim delta = CType(CType(choices(0), Dictionary(Of String, Object))("delta"), Dictionary(Of String, Object))
                                    If delta IsNot Nothing AndAlso delta.ContainsKey("content") Then
                                        Dim piece = CStr(delta("content"))
                                        sb.Append(piece)
                                        Await PostToWebAsync(New Dictionary(Of String, Object) From {
                                            {"type", "appendAssistantChunk"}, {"sessionId", sid}, {"content", piece}
                                        })
                                    End If
                                End If
                            End If
                        Catch
                            ' 忽略 SSE keepalive 垃圾行
                        End Try
                    End If
                Loop
            End Using

            Dim finalText = sb.ToString()
            history.Add(New Dictionary(Of String, Object) From {{"role", "assistant"}, {"content", finalText}})
            Await PostToWebAsync(New Dictionary(Of String, Object) From {
                {"type", "finishAssistantMessage"}, {"sessionId", sid}
            })
        End Using

    Catch hre As HttpRequestException
        Dim diag As New StringBuilder()
        diag.Append("SendAsync 失败（HttpRequestException）：").Append(hre.Message)

        Dim wex = TryCast(hre.InnerException, WebException)
        If wex IsNot Nothing Then
            diag.Append(Environment.NewLine).
                Append("WebException.Status=").Append(wex.Status.ToString()).
                Append(" | ").Append(wex.Message)
            Try
                If wex.Response IsNot Nothing Then
                    Using rs = wex.Response.GetResponseStream()
                        Using sr As New StreamReader(rs, Encoding.UTF8)
                            Dim more = sr.ReadToEnd()
                            If Not String.IsNullOrEmpty(more) Then
                                diag.Append(Environment.NewLine).Append("Response: ").Append(more)
                            End If
                        End Using
                    End Using
                End If
            Catch
                ' 忽略解析响应体失败
            End Try
        End If

        Dim sex = TryCast(hre.InnerException, SocketException)
        If sex IsNot Nothing Then
            diag.Append(Environment.NewLine).
                Append("SocketError=").Append(sex.SocketErrorCode.ToString()).
                Append(" (").Append(sex.Message).Append(")")
        End If

        diag.Append(Environment.NewLine).
            Append("URL=").Append(url).
            Append(" | MODEL=").Append(model)

        errorToPost = New Dictionary(Of String, Object) From {
            {"type", "addAssistantMessage"},
            {"sessionId", sid},
            {"content", diag.ToString()}
        }

    Catch ex As Exception
        errorToPost = New Dictionary(Of String, Object) From {
            {"type", "addAssistantMessage"},
            {"sessionId", sid},
            {"content", "异常: " & ex.Message}
        }
    End Try

    ' —— 在 Try/Catch 结束后再 Await，避免“Catch 内不能 Await”的编译错误 ——
    If errorToPost IsNot Nothing Then
        Await PostToWebAsync(errorToPost)
        Return
    End If
End Function
    '    Private Async Function StreamChatAsync(history As List(Of Dictionary(Of String, Object)), sid As String) As Task
    '        Dim url = baseUrl.TrimEnd("/"c) & "/chat/completions"
    '        Dim req As New HttpRequestMessage(HttpMethod.Post, url)
    '        req.Headers.Authorization = New Headers.AuthenticationHeaderValue("Bearer", apiKey)
    '        req.Headers.Accept.ParseAdd("text/event-stream")
    '
    '        Dim body = New Dictionary(Of String, Object) From {
    '            {"model", model},
    '            {"messages", history.ToArray()},
    '            {"temperature", 0.7},
    '            {"stream", True}
    '        }
    '        Dim ser As New JavaScriptSerializer()
    '        req.Content = New StringContent(ser.Serialize(body), Encoding.UTF8, "application/json")
    '
    '        Using resp = Await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead)
    '            Dim stream = Await resp.Content.ReadAsStreamAsync()
    '            If Not resp.IsSuccessStatusCode Then
    '                Dim textErr = New StreamReader(stream, Encoding.UTF8).ReadToEnd()
    '                Throw New Exception("HTTP " & CInt(resp.StatusCode) & ": " & textErr)
    '            End If
    '
    '            Dim sb As New StringBuilder()
    '            Using sr As New StreamReader(stream, Encoding.UTF8)
    '                Dim line As String = Nothing
    '                Await PostToWebAsync(New Dictionary(Of String, Object) From {{"type", "startAssistantMessage"}, {"sessionId", sid}})
    '                Do
    '                    line = Await sr.ReadLineAsync()
    '                    If line Is Nothing Then Exit Do
    '                    If line.StartsWith("data: ") Then
    '                        Dim payload = line.Substring(6).Trim()
    '                        If payload = "[DONE]" Then Exit Do
    '                        If payload.Length = 0 Then Continue Do
    '                        Try
    '                            Dim chunk = CType(ser.DeserializeObject(payload), Dictionary(Of String, Object))
    '                            If chunk.ContainsKey("choices") Then
    '                                Dim choices = CType(chunk("choices"), Object())
    '                                If choices.Length > 0 Then
    '                                    Dim delta = CType(CType(choices(0), Dictionary(Of String, Object))("delta"), Dictionary(Of String, Object))
    '                                    If delta IsNot Nothing AndAlso delta.ContainsKey("content") Then
    '                                        Dim piece = CStr(delta("content"))
    '                                        sb.Append(piece)
    '                                        Await PostToWebAsync(New Dictionary(Of String, Object) From {{"type", "appendAssistantChunk"}, {"sessionId", sid}, {"content", piece}})
    '                                    End If
    '                                End If
    '                            End If
    '                        Catch
    '                            ' ignore malformed SSE keepalive
    '                        End Try
    '                    End If
    '                Loop
    '            End Using
    '            Dim finalText = sb.ToString()
    '            history.Add(New Dictionary(Of String, Object) From {{"role", "assistant"}, {"content", finalText}})
    '            Await PostToWebAsync(New Dictionary(Of String, Object) From {{"type", "finishAssistantMessage"}, {"sessionId", sid}})
    '        End Using
    '    End Function

    Private Function PostToWebAsync(obj As Object) As Task
        ' Marshal to UI thread; CoreWebView2 must be used on the thread that created it.
        Dim ser As New JavaScriptSerializer()
        Dim jsonText As String = ser.Serialize(obj)
        Dim action As Action =
        Sub()
            Try
                If web IsNot Nothing AndAlso web.CoreWebView2 IsNot Nothing Then
                    web.CoreWebView2.PostWebMessageAsString(jsonText)
                End If
            Catch
                ' 忽略单次 UI 发布失败，避免打断流程
            End Try
        End Sub
        Try
            If web Is Nothing OrElse web.IsDisposed Then
                Return Task.CompletedTask
            End If
            If web.InvokeRequired Then
                web.BeginInvoke(action)
            Else
                action()
            End If
        Catch
            ' 忽略跨线程调用异常
        End Try

        Return Task.CompletedTask
    End Function

End Class